package netty.tcp.websocket;

import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.ReferenceCountUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.net.InetSocketAddress;

import static io.netty.handler.codec.http.HttpHeaderNames.HOST;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.METHOD_NOT_ALLOWED;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;

/**
 * 根据message
 *
 * @author god.yang
 * @date 2021/8/9
 */
@Slf4j
public class MessageInHandler extends ChannelInboundHandlerAdapter {


    private static final String PING = "PING";
    private static final String PREFIX_WS = "ws://";
    private WebSocketServerHandshaker handShaker = null;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // 用户首次建立连接,都是先http请求
        if (msg instanceof FullHttpRequest fullHttpRequest) {
            this.handleFullHttpReq(ctx, fullHttpRequest);
            return;
        }

        ReferenceCountUtil.release(msg);
        //走到这里的,是无法识别的消息类型,不做任何处理
        log.warn("channelId: {}, MessageInHandler Not support type:{}", ctx.channel().id().asLongText(), msg.getClass().getName());
    }

    /**
     * 处理fullHttpRequest请求,握手动作
     */
    private void handleFullHttpReq(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) {
        try {
            // http解码
            if (!fullHttpRequest.decoderResult().isSuccess()) {
                sendHttpRespAndClose(ctx, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
                return;
            }
            // 只允许http get
            if (fullHttpRequest.method() != GET) {
                sendHttpRespAndClose(ctx, new DefaultFullHttpResponse(HTTP_1_1, METHOD_NOT_ALLOWED));
                return;
            }

            // 获取连接的真实IP，即 Client IP
            String clientIP = clientIP(fullHttpRequest, ctx.channel());

            WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(getWebSocketLocation(fullHttpRequest),
                    null, true);
            handShaker = wsFactory.newHandshaker(fullHttpRequest);
            if (handShaker == null) {
                WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel())
                        .addListener(ChannelFutureListener.CLOSE);
                return;
            }
            // 握手协议完成
            handShaker.handshake(ctx.channel(), fullHttpRequest);

            // 握手完成后创建 session ，并添加到 session 管理器
            String useId = fullHttpRequest.headers().get("USER_ID");

            Session session = new Session(Integer.parseInt(useId), ctx.channel());

            SessionManager.instance().store(session);
        } catch (Exception e) {
            log.error("MessageInHandler handleFullHttpReq error, channelId: {}", ctx.channel().id().asLongText(), e);
            sendHttpRespAndClose(ctx, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST,
                    Unpooled.wrappedBuffer(BAD_REQUEST.toString().getBytes())));
        } finally {
            fullHttpRequest.release();
        }
    }


    /**
     * 获取获取连接的真实IP
     * 参考：https://www.runoob.com/w3cnote/http-x-forwarded-for.html
     */
    private String clientIP(FullHttpRequest fullHttpRequest, Channel channel) {
        String clientIP;

        String XFF = fullHttpRequest.headers().get("X-Forwarded-For");

        if (StringUtils.isNotEmpty(XFF)) {
            log.info("Client的X-Forwarded-For: {}, channelId: {}", XFF, channel.id().asLongText());

            // XFF 的内容由「英文逗号 + 空格」隔开的多个部分组成
            String[] ipList = XFF.split(", ");
            // bybit 约定: 如果小于四个取第一个，如果大于四个取倒数第四个
            if (0 < ipList.length && ipList.length <= 4) {
                clientIP = ipList[0];
                return clientIP;
            }
            if (ipList.length > 4) {
                clientIP = ipList[ipList.length - 4];
                return clientIP;
            }
        }

        // 如果没有从XFF中取得真实IP，则从 channel 中获取
        InetSocketAddress inetSocketAddress = (InetSocketAddress) channel.remoteAddress();
        clientIP = inetSocketAddress.getAddress().getHostAddress();
        log.info("Client的X-Forwarded-For没有获取到, 从channelId: {} 的remoteAddress获取: {}", channel.id().asLongText(), clientIP);

        return clientIP;
    }

    /**
     * 处理 PingWebSocketFrame 请求
     */
    private void handlePingWebSocketFrame(ChannelHandlerContext ctx, PingWebSocketFrame pingWebSocketFrame) {
        try {
            ctx.channel().writeAndFlush(new PongWebSocketFrame());
        } finally {
            pingWebSocketFrame.release();
        }
    }

    /**
     * 处理 CloseWebSocketFrame 请求
     */
    private void handleCloseWebSocketFrame(ChannelHandlerContext ctx, CloseWebSocketFrame closeWebSocketFrame) {
        try {
            handShaker.close(ctx.channel(), closeWebSocketFrame.retain());
        } catch (Exception e) {
            log.error("MessageInHandler handleCloseWebSocketFrame error, channelId: {}", ctx.channel().id().asLongText(), e);
        } finally {
            closeWebSocketFrame.release();
        }
    }

    private static String getWebSocketLocation(FullHttpRequest req) {
        return PREFIX_WS + req.headers().get(HOST);
    }

    /**
     * 响应客户端http,并关闭连接
     */
    private void sendHttpRespAndClose(ChannelHandlerContext ctx, FullHttpResponse res) {
        HttpUtil.setContentLength(res, res.content().readableBytes());
        ctx.channel().writeAndFlush(res);

        ctx.close().addListener(future -> {
            log.warn("关闭连接: {}, HTTP响应码: {}", ctx.channel().id().asLongText(), res.status().code());
        });
    }
}
